Skip to content

Reuse compiled standard providers across runs - #619

Open
dnagoda wants to merge 2 commits into
mainfrom
dc.compile-once
Open

dnagoda wants to merge 2 commits into
mainfrom
dc.compile-once

Conversation

@dnagoda

@dnagoda dnagoda commented Oct 1, 2026 •

Copy link
Copy Markdown

Why

Each engine::run call compiles the embedded standard provider (Javy plugin or Shopify Function provider) from its bytes. Even with a warm Wasmtime cache, that takes 8 to 12 ms for the Javy providers, so a caller that runs the same Function many times pays it on every run. It is also the main cost when many inputs run in one process (#620).

What

  • run keeps the compiled providers for the most recent Engine in a private cache. It reuses them while callers pass the same engine (Engine::same).
  • A call with a different engine replaces the cache, so the cache holds one engine at most. A compiled Module keeps its Engine alive, so an unbounded cache would keep every engine a caller ever used.
  • The public API does not change. run and FunctionRunParams keep their signatures, and ValidatedModule stays private.

Performance

1,000 run calls with one engine in one process. Release build, Apple M4 Pro, warm Wasmtime compilation cache.

Fixture Provider main This PR
js_function_javy_plugin_v3.wasm shopify_functions_javy_v3 12.21 ms/run 0.047 ms/run
js_function.wasm javy_quickjs_provider_v1 12.60 ms/run 0.117 ms/run
exit_code.wasm none 0.043 ms/run 0.045 ms/run

A single CLI run still compiles the provider once. Median of 200 process runs with the Javy plugin v3 fixture: 17.8 ms on main, 17.0 ms with this PR. The --json output is identical.

Precompiling the providers at build time, as runtime-engine does, would cut that first load to about 1 ms. It would also add about 39 MB to the binary, and it does not change the cost per run once a provider is loaded. It is not part of this PR.

Testing

  • cargo test --locked: 34 unit tests and 23 integration tests pass; one existing test remains ignored.
  • New unit tests check that the cache reuses a provider for the same engine, keeps each provider separately, and holds only the most recent engine.
  • A new engine test runs a WASI Javy Function and a Javy plugin Function on one engine, on a second engine, and on the first engine again. Every result matches the first run.
  • cargo clippy --locked -- -D warnings and cargo fmt --all -- --check pass.

`run` compiled the embedded standard provider (Javy plugin or Shopify
Function provider) from its bytes on every call. Even with a warm
Wasmtime cache, the Javy providers took about 8 ms per call, so callers
that run the same Function many times paid that cost on every run.

Keep the compiled providers for the most recent engine in a private
cache, and reuse them while callers pass the same engine. A call with a
different engine replaces the cache, so it holds one engine at most.
The public API does not change.
@dnagoda dnagoda changed the title Compile the standard provider once per module Reuse compiled standard providers across runs Oct 2, 2026
The provider cache held its mutex while Module::from_binary compiled a
cold provider, so every other lookup waited, including warm cache hits.

Look up the provider under the lock, compile it without the lock, and
take the lock again to insert it. If the cache moved to another engine
during compilation, return the module without caching it, so the cache
still holds one engine at most.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant